package com.tsyz.servlet.filter;

import com.alibaba.fastjson2.JSON;
import com.tsyz.servlet.util.TraceIdUtil;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.MDC;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.*;

/**
 * 过滤器，用于记录请求和响应的详细信息，包括请求头、请求体、响应头、响应体等
 * 同时生成并传递traceId(TraceId)，便于全链路追踪
 *
 * @author ext.zhaoke14
 */
@Slf4j
@WebFilter(filterName = "tsyzFilter", urlPatterns = "/api/*")
public class TsyzFilter implements Filter {

    // 定义需要记录请求体的Content-Type
    private static final String[] ALLOWED_CONTENT_TYPES = {"application/json", "application/xml", "text/xml", "text/plain", "application/x-www-form-urlencoded"};

    // 定义不需要记录请求体的路径（通常是静态资源）
    private static final String[] EXCLUDED_PATHS = {"/static/", "/assets/", "/favicon.ico"};

    /**
     * 过滤器初始化方法
     *
     * @param filterConfig 过滤器配置
     */
    @Override
    public void init(FilterConfig filterConfig) {
        log.info("[过滤器] TsyzFilter 初始化完成");
    }

    /**
     * 过滤器销毁方法
     */
    @Override
    public void destroy() {
        log.info("[过滤器] TsyzFilter 销毁完成");
    }

    /**
     * 过滤器核心方法，处理请求和响应
     *
     * @param request  请求对象
     * @param response 响应对象
     * @param chain    过滤器链
     * @throws IOException      IO异常
     * @throws ServletException Servlet异常
     */
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        // 记录请求开始时间
        long startTime = System.currentTimeMillis();

        // 使用统一方法初始化TraceId
        String traceId = TraceIdUtil.initTraceId();

        HttpServletRequest httpRequest = (HttpServletRequest) request;
        HttpServletResponse httpResponse = (HttpServletResponse) response;

        // 将traceId放入MDC（Mapped Diagnostic Context），便于日志输出
        MDC.put(TraceIdUtil.TRACE_ID, traceId);

        // 包装请求和响应以便多次读取（原始请求和响应的body只能读取一次）
        RequestWrapper requestWrapper = new RequestWrapper(httpRequest);
        ResponseWrapper responseWrapper = new ResponseWrapper(httpResponse);

        // 记录请求信息
        logRequest(requestWrapper, traceId, startTime);

        try {
            // 调用过滤器链，将请求传递给下一个过滤器或Servlet
            chain.doFilter(requestWrapper, responseWrapper);
        } catch (Exception e) {
            log.error("[traceId:{}] [请求异常] 错误信息={}, 异常类型={}", traceId, e.getMessage(), e.getClass().getName(), e);
            throw e;
        } finally {
            // 记录响应信息
            logResponse(responseWrapper, traceId, startTime);

            // 将捕获的响应内容写回原始响应对象
            responseWrapper.copyBodyToResponse();

            // 清理MDC中的所有数据，防止内存泄漏和线程复用时的数据污染
            TraceIdUtil.clear();
            MDC.clear();
        }
    }


    /**
     * 记录请求信息
     *
     * @param request   包装后的请求对象
     * @param traceId   traceId
     * @param startTime 请求开始时间
     */
    private void logRequest(RequestWrapper request, String traceId, long startTime) {
        try {
            String method = request.getMethod();
            String uri = request.getRequestURI();
            String queryString = request.getQueryString();
            String clientIP = getClientIP(request);
            String userAgent = request.getHeader("User-Agent");

            // 使用LinkedHashMap保持字段顺序
            Map<String, Object> requestInfo = new LinkedHashMap<>();
            requestInfo.put("traceId", traceId);
            requestInfo.put("请求时间", new java.util.Date(startTime));
            requestInfo.put("请求方法", method);
            requestInfo.put("请求路径", uri);
            requestInfo.put("查询参数", queryString);
            requestInfo.put("客户端IP", clientIP);
            requestInfo.put("用户代理", userAgent);

            // 添加请求头信息（过滤敏感信息）
            Map<String, String> headers = getSafeHeaders(request);
            if (!headers.isEmpty()) {
                requestInfo.put("请求头", headers);
            }

            // 对于非GET/DELETE请求，记录请求体（如果适用）
            if (shouldLogBody(uri, request.getContentType()) && !"GET".equals(method) && !"DELETE".equals(method)) {
                String requestBody = request.getRequestBody();
                if (StringUtils.hasText(requestBody)) {
                    // 限制请求体长度，防止日志过大
                    if (requestBody.length() > 2000) {
                        requestInfo.put("请求体", requestBody.substring(0, 2000) + "...(已截断)");
                    } else {
                        requestInfo.put("请求体", requestBody);
                    }
                }
            }

            log.info("[请求入口] {}", JSON.toJSONString(requestInfo));
        } catch (Exception e) {
            log.warn("[请求日志记录错误] 失败原因: {}", e.getMessage());
        }
    }

    /**
     * 记录响应信息
     *
     * @param response  包装后的响应对象
     * @param traceId   traceId
     * @param startTime 请求开始时间
     */
    private void logResponse(ResponseWrapper response, String traceId, long startTime) {
        try {
            long duration = System.currentTimeMillis() - startTime;
            int status = response.getStatus();

            Map<String, Object> responseInfo = new LinkedHashMap<>();
            responseInfo.put("traceId", traceId);
            responseInfo.put("处理耗时", duration + "毫秒");
            responseInfo.put("响应状态", status);

            // 添加响应头信息（过滤敏感信息）
            Map<String, String> headers = getResponseHeaders(response);
            if (!headers.isEmpty()) {
                responseInfo.put("响应头", headers);
            }

            // 记录响应体（如果是JSON或文本类型且大小合适）
            String contentType = response.getContentType();
            if (shouldLogResponseBody(contentType)) {
                String responseBody = response.getResponseBody();
                if (StringUtils.hasText(responseBody)) {
                    // 限制响应体长度，防止日志过大
                    if (responseBody.length() > 2000) {
                        responseInfo.put("响应体", responseBody.substring(0, 2000) + "...(已截断)");
                    } else {
                        responseInfo.put("响应体", responseBody);
                    }
                }
            }

            // 根据状态码决定日志级别
            if (status >= 200 && status < 300) {
                log.info("[请求出口] {}", JSON.toJSONString(responseInfo));
            } else if (status >= 400 && status < 500) {
                log.warn("[traceId:{}] [请求出口] {}", traceId, JSON.toJSONString(responseInfo));
            } else if (status >= 500) {
                log.error("[traceId:{}] [请求出口] {}", traceId, JSON.toJSONString(responseInfo));
            } else {
                log.info("[traceId:{}] [请求出口] {}", traceId, JSON.toJSONString(responseInfo));
            }
        } catch (Exception e) {
            log.warn("[traceId:{}] [响应日志记录错误] 失败原因: {}", traceId, e.getMessage());
        }
    }

    /**
     * 获取客户端真实IP地址（考虑代理情况）
     *
     * @param request 请求对象
     * @return 客户端真实IP
     */
    private String getClientIP(HttpServletRequest request) {
        String[] headers = {"X-Forwarded-For", "Proxy-Client-IP", "WL-Proxy-Client-IP", "HTTP_CLIENT_IP", "HTTP_X_FORWARDED_FOR"};

        for (String header : headers) {
            String ip = request.getHeader(header);
            if (StringUtils.hasText(ip) && !"unknown".equalsIgnoreCase(ip)) {
                // 对于X-Forwarded-For可能有多个IP的情况，取第一个
                return ip.contains(",") ? ip.split(",")[0].trim() : ip;
            }
        }

        return request.getRemoteAddr();
    }

    /**
     * 获取安全的请求头信息（过滤敏感信息如Authorization等）
     *
     * @param request 请求对象
     * @return 过滤后的请求头Map
     */
    private Map<String, String> getSafeHeaders(HttpServletRequest request) {
        Map<String, String> headers = new HashMap<>();
        Enumeration<String> headerNames = request.getHeaderNames();

        // 定义需要过滤的敏感请求头
        Set<String> sensitiveHeaders = new HashSet<>(Arrays.asList("authorization", "cookie", "proxy-authorization", "x-api-key"));

        while (headerNames.hasMoreElements()) {
            String headerName = headerNames.nextElement();
            if (!sensitiveHeaders.contains(headerName.toLowerCase())) {
                headers.put(headerName, request.getHeader(headerName));
            }
        }
        return headers;
    }

    /**
     * 获取响应头信息（过滤敏感信息）
     *
     * @param response 响应对象
     * @return 过滤后的响应头Map
     */
    private Map<String, String> getResponseHeaders(HttpServletResponse response) {
        Map<String, String> headers = new HashMap<>();

        // 定义需要过滤的敏感响应头
        Set<String> sensitiveHeaders = new HashSet<>(Arrays.asList("authorization", "set-cookie", "proxy-authenticate"));

        for (String headerName : response.getHeaderNames()) {
            if (!sensitiveHeaders.contains(headerName.toLowerCase())) {
                headers.put(headerName, response.getHeader(headerName));
            }
        }
        return headers;
    }

    /**
     * 判断是否需要记录请求体
     *
     * @param requestUri  请求URI
     * @param contentType 请求Content-Type
     * @return 是否需要记录请求体
     */
    private boolean shouldLogBody(String requestUri, String contentType) {
        // 排除静态资源
        for (String excludedPath : EXCLUDED_PATHS) {
            if (requestUri.startsWith(excludedPath)) {
                return false;
            }
        }

        // 检查是否是特定文件扩展名的静态资源
        String[] staticExtensions = {".css", ".js", ".png", ".jpg", ".jpeg", ".gif", ".html", ".ico"};
        for (String extension : staticExtensions) {
            if (requestUri.endsWith(extension)) {
                return false;
            }
        }

        // 检查Content-Type是否在允许的范围内
        if (StringUtils.hasText(contentType)) {
            for (String allowedType : ALLOWED_CONTENT_TYPES) {
                if (contentType.contains(allowedType)) {
                    return true;
                }
            }
        }

        return false;
    }

    /**
     * 判断是否需要记录响应体
     *
     * @param contentType 响应Content-Type
     * @return 是否需要记录响应体
     */
    private boolean shouldLogResponseBody(String contentType) {
        if (!StringUtils.hasText(contentType)) {
            return false;
        }

        // 不记录HTML类型的响应体
        if (contentType.contains("text/html")) {
            return false;
        }

        // 只记录JSON、文本和XML类型的响应体
        return contentType.contains("application/json") || contentType.contains("text/") || contentType.contains("application/xml");
    }


    /**
     * 请求包装类，允许多次读取请求体
     * 原始HttpServletRequest的输入流只能读取一次，包装后可以多次读取
     */
    private static class RequestWrapper extends HttpServletRequestWrapper {
        // 请求正文
        private String requestBody;
        // 请求正文读取
        private boolean requestBodyRead = false;

        public RequestWrapper(HttpServletRequest request) {
            super(request);
        }

        @Override
        public ServletInputStream getInputStream() throws IOException {
            if (!requestBodyRead) {
                requestBody = readRequestBody(super.getInputStream());
                requestBodyRead = true;
            }

            if (requestBody == null) {
                return super.getInputStream();
            }

            // 基于缓存的请求体创建新的输入流
            ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(requestBody.getBytes(StandardCharsets.UTF_8));
            return new ServletInputStream() {
                /**
                 * 判断字节数组输入流是否已读取完毕。
                 * @return true 如果字节数组输入流已读取完毕，否则 false。
                 */
                @Override
                public boolean isFinished() {
                    return byteArrayInputStream.available() == 0;
                }

                /**
                 * 判断当前对象是否准备就绪。
                 * @return 总是返回 true。
                 */
                @Override
                public boolean isReady() {
                    return true;
                }

                /**
                 * 设置读取监听器。
                 * @param readListener 读取监听器对象
                 */
                @Override
                public void setReadListener(ReadListener readListener) {
                    // 不需要实现
                }

                /**
                 * 从字节数组输入流中读取一个字节并返回。
                 * @return 读取的字节，或者如果到达流末尾则返回 -1。
                 */
                @Override
                public int read() {
                    return byteArrayInputStream.read();
                }
            };
        }

        @Override
        public BufferedReader getReader() throws IOException {
            return new BufferedReader(new InputStreamReader(getInputStream()));
        }

        /**
         * 获取请求体内容
         *
         * @return 请求体字符串
         * @throws IOException IO异常
         */
        public String getRequestBody() throws IOException {
            if (!requestBodyRead) {
                requestBody = readRequestBody(super.getInputStream());
                requestBodyRead = true;
            }
            return requestBody;
        }

        /**
         * 从输入流读取请求体
         *
         * @param inputStream 输入流
         * @return 请求体字符串
         * @throws IOException IO异常
         */
        private String readRequestBody(ServletInputStream inputStream) throws IOException {
            if (inputStream == null) {
                return null;
            }

            StringBuilder sb = new StringBuilder();
            BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8));
            String line;
            while ((line = reader.readLine()) != null) {
                sb.append(line);
            }
            return sb.toString();
        }
    }

    /**
     * 响应包装类，用于捕获响应体
     * 原始HttpServletResponse的输出流只能写入一次，包装后可以捕获响应内容
     */
    private static class ResponseWrapper extends HttpServletResponseWrapper {
        private final ByteArrayOutputStream capture;  // 用于捕获响应内容的字节数组输出流
        private ServletOutputStream output;  // 包装的输出流
        private PrintWriter writer;  // 包装的打印写入器

        public ResponseWrapper(HttpServletResponse response) {
            super(response);
            capture = new ByteArrayOutputStream(response.getBufferSize());
        }

        @Override
        public ServletOutputStream getOutputStream() {
            if (writer != null) {
                throw new IllegalStateException("getWriter() 已经呼吁对此做出回应。");
            }

            if (output == null) {
                output = new ServletOutputStream() {
                    /**
                     * 重写父类的write方法，向捕获的流中写入一个字节。
                     * @param b 要写入的字节
                     * @throws IOException 如果发生I/O错误
                     */
                    @Override
                    public void write(int b) throws IOException {
                        capture.write(b);
                    }

                    /**
                     * 检查当前对象是否准备就绪。
                     * @return false，表示当前对象未准备就绪。
                     */
                    @Override
                    public boolean isReady() {
                        return false;
                    }

                    /**
                     * 设置写入监听器。
                     * @param writeListener 写入监听器对象。
                     */
                    @Override
                    public void setWriteListener(WriteListener writeListener) {
                        // 不需要实现
                    }
                };
            }

            return output;
        }

        /**
         * 获取响应的打印机对象，用于向客户端发送文本数据。
         *
         * @return 响应的打印机对象
         * @throws IOException 如果在创建打印机对象时发生I/O错误
         */
        @Override
        public PrintWriter getWriter() throws IOException {
            if (output != null) {
                throw new IllegalStateException("getOutputStream（） 已在此响应上调用。");
            }

            if (writer == null) {
                writer = new PrintWriter(new OutputStreamWriter(capture, getCharacterEncoding()));
            }

            return writer;
        }

        /**
         * 强制刷新输出缓冲区。
         *
         * @throws IOException 如果发生I/O错误
         */
        @Override
        public void flushBuffer() throws IOException {
            super.flushBuffer();

            if (writer != null) {
                writer.flush();
            } else if (output != null) {
                output.flush();
            }
        }

        /**
         * 获取捕获的响应内容字节数组
         *
         * @return 字节数组
         * @throws IOException IO异常
         */
        public byte[] getCaptureAsBytes() throws IOException {
            if (writer != null) {
                writer.close();
            } else if (output != null) {
                output.close();
            }

            return capture.toByteArray();
        }

        /**
         * 获取捕获的响应内容字符串
         *
         * @return 响应内容字符串
         * @throws IOException IO异常
         */
        public String getResponseBody() throws IOException {
            // 使用响应的字符编码来正确解码字节数组
            String characterEncoding = getCharacterEncoding();
            if (characterEncoding.isEmpty()) {
                characterEncoding = StandardCharsets.UTF_8.name();
            }
            return new String(getCaptureAsBytes(), characterEncoding);
        }


        /**
         * 将捕获的响应体写入到输出流中。
         *
         * @throws IOException 如果写入过程中发生I/O错误。
         */
        public void copyBodyToResponse() throws IOException {
            if (writer != null) {
                writer.flush();
            } else if (output != null) {
                output.flush();
            }

            // 统一处理响应写入逻辑
            byte[] captureBytes = getCaptureAsBytes();
            ServletOutputStream outputStream = getResponse().getOutputStream();
            try {
                outputStream.write(captureBytes);
            } finally {
                // 确保输出流正确处理
                outputStream.flush();
            }
        }


        /**
         * 获取响应的字符编码
         *
         * @return 字符编码
         */
        @Override
        public String getCharacterEncoding() {
            String encoding = super.getCharacterEncoding();
            if (encoding == null || encoding.isEmpty()) {
                return StandardCharsets.UTF_8.name();
            }
            return encoding;
        }
    }
}
